﻿using MongoDB.Bson;
using MongoDB.Driver;
using Sustainalytics.Entities.ScreeningTool;
using Sustainalytics.ScreeningTool.Synchronization.Caches;
using Sustainalytics.ScreeningTool.Synchronization.Processors;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Sustainalytics.ScreeningTool.Synchronization
{
    public class ExtraElementsCreator
    {
        private readonly ResearchStatesCache _researchStatesCache;
        private readonly FieldDefinitionsCache _fieldDefinitionsCache;
        private readonly CompanyIdsCache _companyIdsCache;
        private readonly IMongoCollection<ScreeningData> _screeningDataCollection;
        private readonly IEnumerable<IProcessor> _processors;

        public ExtraElementsCreator(
            ResearchStatesCache researchStatesCache,
            FieldDefinitionsCache fieldDefinitionsCache,
            CompanyIdsCache companyIdsCache,
            IMongoCollection<ScreeningData> screeningDataCollection,
            IEnumerable<IProcessor> processors)
        {
            _researchStatesCache = researchStatesCache;
            _fieldDefinitionsCache = fieldDefinitionsCache;
            _companyIdsCache = companyIdsCache;
            _screeningDataCollection = screeningDataCollection;
            _processors = processors;
        }

        public async Task AddExtraElementsForResearchEntitiesAsync()
        {
            await _processors.ParallelForEachAsync(
                processor => processor.ForEachExtraElementsAsync(ProcessExtraElements),
                Environment.ProcessorCount / 2);
        }

        private async Task ProcessExtraElements(IEnumerable<ExtraElement> extraElements, Guid companyId)
        {
            var companyIsCoverage = _companyIdsCache.IsCoverage(companyId);
            var researchStates = _researchStatesCache.GetResearchStates(companyId);
            var filteredExtraElements = FilterExtraElements(extraElements, researchStates, companyIsCoverage);
            if (filteredExtraElements.Any())
            {
                var update = GetUpdateDefinition(filteredExtraElements);
                await _screeningDataCollection.UpdateOneAsync(x => x.Id == companyId, update).ConfigureAwait(false);
            }
        }

        private IEnumerable<ExtraElement> FilterExtraElements(IEnumerable<ExtraElement> extraElements, ResearchStates researchStates, bool companyIsCoverage)
        {
            if (extraElements == null)
                return Enumerable.Empty<ExtraElement>();

            return extraElements.Where(extraElement =>
                extraElement != null &&
                extraElement.Value != null &&
                extraElement.Value as BsonValue != BsonNull.Value &&
                _fieldDefinitionsCache.ContainsField(extraElement.Key, companyIsCoverage) &&
                _fieldDefinitionsCache.IsResearchAvailable(extraElement.Key, researchStates));
        }

        public async Task AddExtraElementsForCoverageEntitiesAsync()
        {
            var builder = Builders<ScreeningData>.Update;
            var researchParentIds = _companyIdsCache.GetResearchParentIds();
            await _screeningDataCollection
                .Find(x => researchParentIds.Contains(x.Id))
                .ForEachAsync(async researchEntity =>
                {
                    var coverageEntityIds = _companyIdsCache.GetCoverageEntityIdsByResearchParentId(researchEntity.Id);
                    var updates = researchEntity.ExtraElements
                        .Where(x => _fieldDefinitionsCache.ContainsFieldWithValueFromParent(x.Name))
                        .Select(x => builder.Set(x.Name, x.Value));

                    if (updates.Any())
                    {
                        var update = builder.Combine(updates);
                        await _screeningDataCollection.UpdateManyAsync(x => coverageEntityIds.Contains(x.Id), update).ConfigureAwait(false);
                    }
                })
                .ConfigureAwait(false);
        }

        private static UpdateDefinition<ScreeningData> GetUpdateDefinition(IEnumerable<ExtraElement> extraElements)
        {
            var builder = Builders<ScreeningData>.Update;
            var updates = extraElements.WithTimestamps().Select(x => builder.Set(x.Key, x.Value));
            return builder.Combine(updates);
        }
    }
}
